package no.sachmuld.methodcombination.demo;

import org.apache.log4j.Logger;
import org.springframework.jdbc.BadSqlGrammarException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;

import javax.sql.DataSource;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.*;

/**
 *
 */
public class DatabaseMigrater {

    public static enum ScriptType { DDL, DML }

    public static class Migration {
        private int version;
        private String description;
        private ScriptType scriptType;
        private String scriptFile;

        public Migration(int version, String description, ScriptType scriptType, String scriptFile) {
            this.version = version;
            this.description = description;
            this.scriptType = scriptType;
            this.scriptFile = scriptFile;
        }

        public int getVersion() {
            return version;
        }

        public String getDescription() {
            return description;
        }

        public ScriptType getScriptType() {
            return scriptType;
        }

        public String getScriptFile() {
            return scriptFile;
        }
    }

    private Logger logger = Logger.getLogger(DatabaseMigrater.class);

    private static final int NO_DATABASE_VERSION = 0;

    public DatabaseMigrater() {
    }

    public void execute(DataSource dataSource, List<Migration> migrations) {

        int currentVersion = findCurrentVersion(dataSource);
        logger.info("Current database version: " + currentVersion);

        for (Migration migration : migrations) {
            if (migration.getVersion() > currentVersion) {
                executeMigration(dataSource, migration);
            } else {
                logger.info("Migration '" + migration.getDescription() + "' already executed. Skipping.");
            }
        }
    }

    private int findCurrentVersion(DataSource dataSource) {
        try {
            return new JdbcTemplate(dataSource).queryForInt("select max(VERSION) from DATABASE_MIGRATIONS");
        } catch (BadSqlGrammarException e) {
            return NO_DATABASE_VERSION;
        }
    }

    private void executeMigration(DataSource dataSource, Migration migration) {
        logger.info("Executing: " + migration.getDescription());

        StringBuilder script = new StringBuilder();
        String scriptPath = migration.getScriptFile();

        URL url = getSQLScriptURL(scriptPath);
        try {
            BufferedReader br = new BufferedReader(new InputStreamReader(url.openStream(), "utf-8"));
            String line = br.readLine();
            while (line != null) {
                script.append(line);
                line = br.readLine();
            }
            br.close();
        } catch (IOException e) {
            logger.error("", e);
        }

        String[] sql = script.toString().split(";");

        if (ScriptType.DDL.equals(migration.getScriptType())) {
            executeDDL(dataSource, sql, migration.getVersion(), migration.getDescription());
        } else if (ScriptType.DML.equals(migration.getScriptType())) {
            executeDML(dataSource, sql, migration.getVersion(), migration.getDescription());
        }

    }

    private URL getSQLScriptURL(String location) {

        try {
            List<URL> urls = Collections.list(getClass().getClassLoader().getResources(location));

            if (urls.size() == 0) {
                String message = "No SQL script found at location: " + location;
                logger.error(message);
                throw new RuntimeException(message);
            } else if (urls.size() > 1) {
                String message = "Duplicate SQL scripts found at location " + location;
                logger.error(message);
                for (URL url : urls) {
                    logger.error(url.getPath());
                }
                throw new RuntimeException(message);
            } else {
                return urls.get(0);
            }
        } catch (IOException e) {
            throw new RuntimeException("Error loading SQL script: " + location, e);
        }
    }

    private void executeDDL(DataSource dataSource, String[] sql, int version, String description) {
        // every DDL statement executes in own transaction
        for (String statement : sql) {
            new JdbcTemplate(dataSource).execute(statement);
        }
        logger.info(sql.length + " sql statements executed");
        insertMigrationsInfo(dataSource, version, description);
    }

    private void executeDML(final DataSource dataSource, final String[] sql, final int version, final String description) {
        TransactionTemplate transactionTemplate = new TransactionTemplate(new DataSourceTransactionManager(dataSource));
        transactionTemplate.execute(new TransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
                new JdbcTemplate(dataSource).batchUpdate(sql);
                logger.info(sql.length + " sql statements executed");
                insertMigrationsInfo(dataSource, version, description);
            }
        });
    }

    private void insertMigrationsInfo(DataSource dataSource, int version, String description) {
        new JdbcTemplate(dataSource).update("insert into DATABASE_MIGRATIONS (VERSION, DESCRIPTION, UPDATED_DATE) values (?, ?, ?)", version, description, new Date());
        logger.info("Updated database migrations info");
    }

}